/**
 * 
 */
package com.devmindset.jpatoolbox;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.apache.log4j.Logger;

/**
 * <pre>
 * Utility class to wire the bidirectionality of a One to One relationship.
 * 
 * This wiring is done according to the following rules and conventions:
 * 
 * Use case: Customer <-> CreditCard.
 * 
 * For this relationship, we can say that it is permitted one customer to
 * have one credit card, and one credit card belongs to one customer.
 * 
 * Also, from the business logic perspective, it makes sense to have relationship
 * from the customer to the credit card, and it makes sense to lookup the customer
 * for the given customer, hence classifying the relationship as bidirectional.
 * 
 * We assume the following:
 * 
 * Both sides have setter methods. For example:
 * 
 * <code>Customer</code> has <code>setCreditCard(CreditCard creditCard) method, and
 * <code>CreditCard</code> has <code>setCustomer(Customer customer)</code> method.
 * 
 *  To wire the sides you use the methods of this class; see the specific methods for
 *  usage details.
 * </pre>
 *
 * @author <a href="mailto:ice.penov@devmindset.com">Ice Penov</a>
 */
@SuppressWarnings("unchecked")
public final class OneToOneUtil {
  
  private static final Logger logger = Logger.getLogger(OneToOneUtil.class);
  
  private OneToOneUtil() {
  }
  
  /**
   * <pre>
   * Wires the One to One relationship, by means of association.
   * 
   * For the Customer <-> CreditCard use case, you use this method in the setter
   * on each side, in this case:
   * 
   * In the Customer class:
   * 
   * public void setCreditCard(CreditCard creditCard) {
   *   OneToOne.wire(this, creditCard, Customer.class, CreditCard.class);
   * }
   * 
   * Similarly, on the other side (in the CreditCard class):
   * 
   * public void setCustomer(Customer customer) {
   *   OneToOne.wire(this, customer, CreditCard.class, Customer.class);
   * }
   * </pre>
   * 
   * @param thisSide
   * @param otherSide
   * @param thisSideClass needed in case thisSide is null
   * @param otherSideClass needed in case otherSide is null
   */
  public static void wire(Object thisSide, Object otherSide, Class thisSideClass, Class otherSideClass) {
    try {
      Method getOtherSide = thisSideClass.getMethod(Composer.composeGetter(otherSideClass), (Class[]) null);
      Method setThisSide = otherSideClass.getMethod(Composer.composeSetter(thisSideClass), thisSideClass);
      Field otherSideField = thisSideClass.getDeclaredField(Composer.getFieldName(otherSideClass));
      
      otherSideField.setAccessible(true);
      if (otherSide == null && getOtherSide.invoke(thisSide, (Object[]) null) != null) {
        Object tmp = getOtherSide.invoke(thisSide, (Object[]) null);
        otherSideField.set(thisSide, null);
        Object n = null;
        setThisSide.invoke(tmp, n);
      } else if (otherSide != null && !otherSide.equals(getOtherSide.invoke(thisSide, (Object[]) null))) {
        otherSideField.set(thisSide, otherSide);
        setThisSide.invoke(otherSide, thisSide);
      }      
    } catch (Exception e) {
      logger.error("Error while wiring 1 to 1 bidirectional relationship", e);
    }    
  }

}
